<?php
/*
 * 
 * @class: IO_File
 * @author: Blagovest Gospodinov
 */

class IO_File
	implements Interface_File{
	
	const NON_STRING_PATH_ERROR = '$path was %s, not a string or a Interface_File';
	const IS_DIRECTORY_ERROR = '%s is already a directory, IO_File does not allow operations on directories';
	const INVALID_UNITS_ERROR = 'Invalid units given (type %s), expects integer or one of the standard (see IO/File.php).';
	const FILE_NOT_FOUND_ERROR = 'File %s was not found';
	
	static private $_units =
		array(
			'bytes' => 1,
			
			'kilobytes' => 1000,
			'megabytes' => 1000000,
			'gigabytes' => 1000000000,
			'terabytes' => 1000000000000,

			'kibibytes' => 1024,
			'mibibytes' => 1048576,
			'gibibytes' => 1073741824,
			'tibibytes' => 1099511627776,
		);
	
	protected $_directoryPart;
	protected $_fileName;
	protected $_options;
	
	public function __construct( $path , $options = array() ){
		$this->_init( $path , $options );
	}
	
	protected function _init( $path , $options ){
		if( $path instanceof Interface_File )
			$path = $path->absolutePath();
		
		/// btw, think of a way to have something nicer
		///	than Object for all classes (e.g. replace gettype..)
		if( !is_string( $path ) )
			throw new Exception_Generic_ValueError( sprintf( self::NON_STRING_PATH_ERROR , gettype($path) ) );
		
		/// Nonsense having a file object on an existing directory
		///	First delete your folder
		///		::? have $options['deleteIfDirectory']
		if( is_dir($path) )
			throw new Exception_Generic_ValueError( sprintf( self::IS_DIRECTORY_ERROR , $path ) );
		
		/// See basename doc, we want /A/B/ to map to A/B
		if( in_array( $path[strlen($path) - 1] , array( DIRECTORY_SEPARATOR , '/') ) )
			$path = substr( $path , 0 , - 1 );
		
		$path = realpath( $path );
		
		list( $this->_directoryPart , $this->_fileName )
			= array( dirname($path) , basename($path) );
		
		$this->_options = $options;
	}
	
	public function absolutePath(){
		/// Use IO_Path::join here! as an improvement later on
		return IO_Path::join( $this->_directoryPart , $this->_fileName ); 
	}
	
	public function exists(){
		return file_exists( $this->absolutePath() );
	}
	
	public function assertExists(){
		if( !$this->exists() )
			throw new Exception_IO_FileNotFoundError( sprintf( self::FILE_NOT_FOUND_ERROR , $this->absolutePath() ) ); /// throw File Not Found exception!		
	}
	
	public function size( $units = 'bytes' ){
		/// Create Unit specific errors here, as we're gonna have a
		/// Converter class later on anyway
		$this->assertExists();
		
		if( is_string($units) )
			$units = strtolower( $units );
		
		if( ( !is_integer($units) and !is_float($units)
				 and !is_string($units) ) or !isset( self::$_units[$units] ) )
			throw new Exception_Generic_ValueError( sprintf( self::INVALID_UNITS_ERROR , gettype($units) ) );
		
		$divisor = ( is_integer($units) or is_float($units) 
						? $units : self::$_units[ strtolower($units) ] );
		
		return filesize( $this->absolutePath() ) / $divisor;
	}
	
	public function isWriteable(){
		$this->assertExists();
		
		return is_writable( $this->absolutePath() );
	}
	
	public function isReadable(){
		$this->assertExists();
		
		return is_readable( $this->absolutePath() );
	}
	
	public function isExecutable(){
		$this->assertExists();
		
		return is_executable( $this->absolutePath() );
	}
	
	public function name(){
		return $this->_fileName;
	}
	
	public function read( $line = null ){
		if( is_array( $line ) ){
			$result = array();

			foreach( $line as $key => $value ){
				$result[$key] = $this->read( $value );		
			}
		}else{
			if( $line === null )
				return file_get_contents( $this->absolutePath() );
			else if( is_integer( $line ) ){
				/// Warning: this is O(N)
				$currline = 0;
				$currline_content = "";
				
				$handle = fopen( $this->absolutePath() , "r" );
				
				if( !$handle )
					throw new Exception_IO_CouldNotOpenFileError();
				
				for( $i = 0 ; $i < $line ; $i++ ){
					$currline_content = fgets( $handle );

					/// If reached the end of file prematurely
					if( $currline_content === false )
						throw new Exception_IO_InvalidLineNumberError();
				}
				
				return $currline_content;
			}else /// invalid line
				throw new Exception_Generic_ValueError();
		}
	}
	
	public function lines( $first = null ){
		$this->assertExists();
		
		if( $first === null )
			return file( $this->absolutePath() );
		else if( is_integer( $first ) ){
			$result = array();

			$handle = fopen( $this->absolutePath() , "r" );
			
			if( !$handle )
				throw new Exception_IO_CouldNotOpenFileError();
			
			for( $i = 0 ; $i < $line ; $i++ ){
				$line = fgets( $handle );
				
				if( $line === false )
					throw new Exception_IO_InvalidLineNumberError();
				
				$result[] = $line;
			}
			
			return $result;
		}else /// ...
			throw new Exception_Generic_ValueError();
	}
	
	public function rename( $new_name ){
		$this->assertExists();
		
		if( $new_name instanceof Interface_File )
			$new_name = $new_name->absolutePath();
		
		if( !is_string( $new_name ) )
			throw new Exception_Generic_ValueError();
		
		/// Try the rename, will fail if file already exists / is directory, etc.
		/// If succeeds, update the object
		if( rename( $this->absolutePath() , $new_name ) )
			$this->_init( $new_name , $this->_options );
		else
			throw new Exception_IO_RenameFailedError();
	}
	
	public function parentDirectory( $asDirectoryObject = true , $options = array() ){
		if( $asDirectoryObject )
			return IO_Directory( $this->_directoryPart , $options );
		else
			return $this->_directoryPart;
	}
	
	public function owner( $asID = true ){
		$this->assertExists();
		
		if( $asID )
			return fileowner( $asID );
		else
			return null; /// not yet implemented, may throw?
	}
}